package scenic;

import java.awt.Rectangle;
import java.awt.geom.AffineTransform;

import scenic.geom.TransformBuilder;
import scenic.jni.Renderer;

public class DoubleBuffer {
	public interface Painter {
		void paint(DrawContext context, AffineTransform newTransform, Rectangle visibleArea);
	}
	
	private static final int margin = 256;
	private Image image;
	private Image secondaryImage;
	private AffineTransform bufferTransform;
	private Rectangle bufferVisibleArea;
	private Painter painter;
	private boolean repaintBuffer;
	private int superSampling = 1;
	
	/*
	 * Updates the buffer. This implementation uses one image for storing the data. When
	 * updating, a new image is created and the contents of the old image are copied
	 * to the new image after which needed regions are painted. After this the old image
	 * can be deleted. 
	 * 
	 * However, because image creation is *very* time consuming, we optimize performance
	 * by reusing images when possible.
	 */
	private void updateBuffer(Image oldImage, DrawContext context, AffineTransform newTransform, Rectangle newVisibleArea,
			Rectangle oldVisibleArea) {
		Image temp = image;
		
		image = secondaryImage;
		secondaryImage = temp;
		
		if(image == null || image.getWidth() != bufferVisibleArea.width || 
				image.getHeight() != bufferVisibleArea.height)
		{
			if(image != null) {
				image.free();
			}
			image = new Image(bufferVisibleArea.width, bufferVisibleArea.height, 
					Format.A8R8G8B8, ImageOptions.RENDER_TARGET | ImageOptions.PREMULTIPLIED_ALPHA);
		}
		context.context = context.renderer.createContextForImage(image.getId());				
		context.renderer.beginScene(context.context);
		if(oldImage != null) {
			int dx = oldVisibleArea.x - bufferVisibleArea.x;
			int dy = oldVisibleArea.y - bufferVisibleArea.y;
			
			context.renderer.color(context.context, 0, 0, 0, 0);
			context.renderer.clear(context.context);
			
			if(dy > 0) {
				Rectangle r = new Rectangle(0, 0, image.getWidth(), dy);
				
				if(context.renderer.beginClip(context.context, r)) {
					context.renderer.color(context.context, 0, 0, 0, 0);
					context.renderer.clear(context.context);
					painter.paint(context, newTransform, r);
					context.renderer.discardSurface(context.context);
				}
			}
			if(dy + oldImage.getHeight() < image.getHeight()) {
				Rectangle r = new Rectangle(0, dy + oldImage.getHeight(), image.getWidth(), 
						image.getHeight() - (dy + oldImage.getHeight()));
				
				if(context.renderer.beginClip(context.context, r)) {
					context.renderer.color(context.context, 0, 0, 0, 0);
					context.renderer.clear(context.context);
					painter.paint(context, newTransform, r);
					context.renderer.discardSurface(context.context);
				}
			}
			if(dx > 0) {
				Rectangle r = new Rectangle(0, 0, dx, image.getHeight());
				
				if(context.renderer.beginClip(context.context, r)) {
					context.renderer.color(context.context, 0, 0, 0, 0);
					context.renderer.clear(context.context);
					painter.paint(context, newTransform, r);
					context.renderer.discardSurface(context.context);
				}
			}
			if(dx + oldImage.getWidth() < image.getWidth()) {
				Rectangle r = new Rectangle(dx + oldImage.getWidth(), 0, 
						image.getWidth() - (dx + oldImage.getWidth()), image.getHeight());
				
				if(context.renderer.beginClip(context.context, r)) {
					context.renderer.color(context.context, 0, 0, 0, 0);
					context.renderer.clear(context.context);
					painter.paint(context, newTransform, r);
					context.renderer.discardSurface(context.context);
				}
			}
			context.renderer.setTransform(context.context, 1.0, 0.0, 0.0, 1.0, dx, dy);
			context.renderer.color(context.context, 1, 1, 1, 1);
			context.renderer.drawImage(context.context, oldImage.getId(),
					0, 0, oldImage.getWidth(), oldImage.getHeight());
//			oldBuffer.free();
		} else {
			context.renderer.color(context.context, 0, 0, 0, 0);
			context.renderer.clear(context.context);
			painter.paint(context, newTransform, newVisibleArea);
		}
		context.renderer.endScene(context.context);
		context.renderer.deleteContext(context.context);
	}
	
	void update(DrawContext context, AffineTransform transform, Rectangle visibleArea) {
		boolean validBuffer = image != null && !image.contentsLost() && !repaintBuffer;
		boolean useOldBuffer = false;
		
		visibleArea = new Rectangle(visibleArea.x / superSampling, visibleArea.y / superSampling, 
				visibleArea.width * superSampling, visibleArea.height * superSampling);
		AffineTransform t = AffineTransform.getScaleInstance(superSampling, superSampling);
		t.concatenate(transform);
		transform = t;
		
		if(validBuffer) {
			int dx = (int)Math.round(transform.getTranslateX() - bufferTransform.getTranslateX());
			int dy = (int)Math.round(transform.getTranslateY() - bufferTransform.getTranslateY());
			
			bufferVisibleArea.translate(dx, dy);
			bufferTransform.setTransform(bufferTransform.getScaleX(),
					bufferTransform.getShearY(),
					bufferTransform.getShearX(),
					bufferTransform.getScaleY(),
					transform.getTranslateX(), 
					transform.getTranslateY());
			if(!equalsLinear(bufferTransform, transform) ||
					!compatibleOffset(bufferTransform, transform) ||
					!bufferVisibleArea.contains(visibleArea)) {
				if(equalsLinear(bufferTransform, transform) ||
						!compatibleOffset(bufferTransform, transform)) {
					useOldBuffer = true;
				}
				validBuffer = false;
			}
		}

		if(!validBuffer) {
			Image oldBuffer = null;
			Rectangle oldVisibleArea = bufferVisibleArea;
			
			if(image != null) {
				if(useOldBuffer)
					oldBuffer = image;
			}
			
			if(useOldBuffer) {
				bufferVisibleArea = (Rectangle)bufferVisibleArea.clone();			
				bufferVisibleArea.width = visibleArea.width + margin;
				bufferVisibleArea.height = visibleArea.height + margin;
				if(bufferVisibleArea.x > visibleArea.x)
					bufferVisibleArea.x = visibleArea.x - margin;
				else if(bufferVisibleArea.getMaxX() < visibleArea.getMaxX())
					bufferVisibleArea.x = visibleArea.x;
				if(bufferVisibleArea.y > visibleArea.y)
					bufferVisibleArea.y = visibleArea.y - margin;
				else if(bufferVisibleArea.getMaxY() < visibleArea.getMaxY())
					bufferVisibleArea.y = visibleArea.y;
			} else {
				bufferVisibleArea = (Rectangle)visibleArea.clone();			
				bufferVisibleArea.x -= margin / 2;
				bufferVisibleArea.y -= margin / 2;
				bufferVisibleArea.width += margin;
				bufferVisibleArea.height += margin;				
			}
			
			DrawContext newContext = new DrawContext(context, Renderer.SURFACE_TYPE_COLOR);
			AffineTransform newTransform = (AffineTransform)transform.clone();
			Rectangle newVisibleArea = new Rectangle(0, 0, bufferVisibleArea.width, bufferVisibleArea.height);
			
			bufferTransform = transform;
			newTransform.setTransform(newTransform.getScaleX(),
					newTransform.getShearY(),
					newTransform.getShearX(),
					newTransform.getScaleY(),
					newTransform.getTranslateX() - bufferVisibleArea.x, 
					newTransform.getTranslateY() - bufferVisibleArea.y);
			
			updateBuffer(oldBuffer, newContext, newTransform, newVisibleArea, oldVisibleArea);
		}
		context.renderer.setTransform(context.context, 
				1.0 / superSampling, 0.0, 0.0, 1.0 / superSampling, 
				bufferVisibleArea.x / superSampling, 
				bufferVisibleArea.y / superSampling);
		context.renderer.color(context.context, 1, 1, 1, 1);
		context.renderer.drawImage(context.context, image.getId(),
				0, 0, image.getWidth(), image.getHeight());
		repaintBuffer = false;
	}
	
	void free() {
		if(image != null) {
			image.free();
			image = null;
		}
	}
	
	public DoubleBuffer(Painter painter) {
		this.painter = painter;
	}
	
	void invalidate() {
		repaintBuffer = true;
	}

	
	private static boolean equalsLinear(AffineTransform a, AffineTransform b) {
		return a.getScaleX() == b.getScaleX() &&
			a.getScaleY() == b.getScaleY() &&
			a.getShearX() == b.getShearX() &&
			a.getShearY() == b.getShearY();
	}
	
/*	private static boolean equalsOffset(Affine2 a, Affine2 b) {
		return a.dx == b.dx &&
			a.dy == b.dy;
	} */
	
	private static boolean compatibleOffset(double a, double b) {
		double d = a - b;
		
		return Math.abs(Math.floor(d + 0.5) - d) < 0.0001;
	}
	
	private static boolean compatibleOffset(AffineTransform a, AffineTransform b) {
		return compatibleOffset(a.getTranslateX(), b.getTranslateX()) &&
			compatibleOffset(a.getTranslateY(), b.getTranslateY());
	}
}
